/**
 * Helpers
 */
( function(exports) {
    var __slice = [].slice;
    var __hasProp = {}.hasOwnProperty;
    var __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
    /* $.extend functionality */
    function extend( target, src )
    {
        var options, name, copy, copyIsArray, clone,
            i = 1,
            length = 2,
            deep = true;

        // Handle a deep copy situation
        if( typeof target === "boolean" )
        {
            deep = target;
            // skip the boolean and the target
            i = 2;
        }

        // Handle case when target is a string or something( possible in deep copy )
        if( typeof target !== "object" && !typeof target === 'function' )
        {
            target = {};
        }
        // Only deal with non-null/undefined values
        if( options = src )
        {
            // Extend the base object
            for( name in options )
            {
                src = target[name];
                copy = options[name];

                // Prevent never-ending loop
                if( target === copy )
                {
                    continue;
                }
                // Recurse if we're merging plain objects or arrays
                if( deep &&( typeof copy == 'object' ||( copyIsArray = Object.prototype.toString.call(  copy  ) === '[object Array]' ) ) )
                {
                    if( copyIsArray )
                    {
                        copyIsArray = false;
                        clone = src && Object.prototype.toString.call(  src  ) === '[object Array]' ? src : [];

                    }
                    else
                    {
                        clone = src && typeof src == 'object' ? src : {};
                    }
                    // Never move original objects, clone them
                    target[name] = extend( clone, copy );

                    // Don't bring in undefined values
                }
                else if( typeof copy !== 'undefined' )
                {
                    target[name] = copy;
                }
            }
        }
        return target;
    }

    // Make available to window
    exports.GameController = {

        // Default options,
        options: {
            left: {
                type: 'dpad',
                position: { left: '13%', bottom: '22%' },
                dpad: {
                    up: {
                        width: '7%',
                        height: '15%',
                        stroke: 2,
                        touchStart: function() {
                            GameController.simulateKeyEvent( 'press', 38 );
                            GameController.simulateKeyEvent( 'down', 38 );
                        },
                        touchEnd: function() {
                            GameController.simulateKeyEvent( 'up', 38 );
                        }
                    },
                    left: {
                        width: '15%',
                        height: '7%',
                        stroke: 2,
                        touchStart: function() {
                            GameController.simulateKeyEvent( 'press', 37 );
                            GameController.simulateKeyEvent( 'down', 37 );
                        },
                        touchEnd: function() {
                            GameController.simulateKeyEvent( 'up', 37 );
                        }
                    },
                    down: {
                        width: '7%',
                        height: '15%',
                        stroke: 2,
                        touchStart: function() {
                            GameController.simulateKeyEvent( 'press', 40 );
                            GameController.simulateKeyEvent( 'down', 40 );
                        },
                        touchEnd: function() {
                            GameController.simulateKeyEvent( 'up', 40 );
                        }
                    },
                    right: {
                        width: '15%',
                        height: '7%',
                        stroke: 2,
                        touchStart: function() {
                            GameController.simulateKeyEvent( 'press', 39 );
                            GameController.simulateKeyEvent( 'down', 39 );
                        },
                        touchEnd: function() {
                            GameController.simulateKeyEvent( 'up', 39 );
                        }
                    }
                },
                joystick: {
                    radius: 60,
                    touchMove: function( e ) {
                        console.log( e );
                    }
                }
            },
            right: {
                type: 'buttons',
                position: { right: '17%', bottom: '28%' },
                buttons: [
                    { offset: { x: '-13%', y: 0 }, label: 'X', radius: '7%', stroke: 2, backgroundColor: 'blue', fontColor: '#fff', touchStart: function() {
                        // Blue is currently mapped to up button
                        GameController.simulateKeyEvent( 'press', 88 ); // x key
                        GameController.simulateKeyEvent( 'down', 88 );
                    }, touchEnd: function() {
                        GameController.simulateKeyEvent( 'up', 88 );
                    } },
                    { offset: { x: 0, y: '-11%' }, label: 'Y', radius: '7%', stroke: 2, backgroundColor: 'yellow', fontColor: '#fff', touchStart: function() {
                        GameController.simulateKeyEvent( 'press', 70 ); // f key
                        GameController.simulateKeyEvent( 'down', 70 );
                    }, touchEnd: function() {
                        GameController.simulateKeyEvent( 'up', 70 );
                    }  },
                    { offset: { x: '13%', y: 0 }, label: 'B', radius: '7%', stroke: 2, backgroundColor: 'red', fontColor: '#fff', touchStart: function() {
                        GameController.simulateKeyEvent( 'press', 90 ); // z key
                        GameController.simulateKeyEvent( 'down', 90 );
                    }, touchEnd: function() {
                        GameController.simulateKeyEvent( 'up', 90 );
                    } },
                    { offset: { x: 0, y: '11%' }, label: 'A', radius: '7%', stroke: 2, backgroundColor: 'green', fontColor: '#fff', touchStart: function() {
                        GameController.simulateKeyEvent( 'press', 67 ); // a key
                        GameController.simulateKeyEvent( 'down', 67 );
                    }, touchEnd: function() {
                        GameController.simulateKeyEvent( 'up', 67 );
                    }  }
                ],
                dpad: {
                    up: {
                        width: '7%',
                        height: '15%',
                        stroke: 2
                    },
                    left: {
                        width: '15%',
                        height: '7%',
                        stroke: 2
                    },
                    down: {
                        width: '7%',
                        height: '15%',
                        stroke: 2
                    },
                    right: {
                        width: '15%',
                        height: '7%',
                        stroke: 2
                    }
                },
                joystick: {
                    radius: 60,
                    touchMove: function( e ) {
                        console.log( e );
                    }
                }
            },
            touchRadius: 45
        },

        // Areas (objects) on the screen that can be touched
        touchableAreas: [],
        touchableAreasCount: 0,

        // Multi-touch
        touches: [],

        // Canvas offset on page (for coverting touch coordinates)
        offsetX: 0,
        offsetY: 0,

        // Bounding box - used for clearRect - first we determine which areas of the canvas are actually drawn to
        bound: {
            left: false,
            right: false,
            top: false,
            bottom: false
        },

        // Heavy sprites (with gradients) are cached as a canvas to improve performance
        cachedSprites: {},

        paused: false,

        init: function( options ) {

            // Don't do anything if there's no touch support
            if( ! 'ontouchstart' in document.documentElement )
                return;


            // Merge default options and specified options
            options = options || {};
            extend( this.options, options );

            var userAgent = navigator.userAgent.toLowerCase();
            // See if we should run the performanceFriendly version (for slower CPUs)
            this.performanceFriendly = ( userAgent.indexOf( 'iphone' ) !== -1 || userAgent.indexOf( 'android' ) !== -1 || this.options.forcePerformanceFriendly );

            // Grab the canvas if one wasn't passed
            var ele;
            if( !this.options.canvas || !( ele = document.getElementById( this.options.canvas ) ) )
            {
                this.options.canvas = document.getElementsByTagName( 'canvas' )[0];
            }
            else if( ele )
            {
                this.options.canvas = ele;
            }

            this.options.ctx = this.options.canvas.getContext( '2d' );

            // Create a canvas that goes directly on top of the game canvas
            this.createOverlayCanvas();
        },

        /**
         * Finds the actual 4 corners of canvas that are being used (so we don't have to clear the entire canvas each render)
         * Called when each new touchableArea is added in
         * @param {object} options - x, y, width, height
         */
        boundingSet: function( options ) {
            var directions = ['left', 'right'];

            // Square - pivot is top left
            if( options.width )
            {
                var width = this.getPixels( options.width );
                var height = this.getPixels( options.height );
                var left = this.getPixels( options.x );
                var top = this.getPixels( options.y );
            }
            // Circle - pivot is center
            else
            {
                if( this.options.touchRadius )
                    var radius = this.getPixels( options.radius ) * 2 + ( this.getPixels( this.options.touchRadius ) / 2 ); // size of the box the joystick can go to
                else
                    var radius = options.radius;
                var width = height = ( radius + this.getPixels( options.stroke ) ) * 2;
                var left = this.getPixels( options.x ) - ( width / 2 );
                var top = this.getPixels( options.y ) - ( height / 2 );
            }
            var right = left + width;
            var bottom = top + height;

            if( this.bound.left === false || left < this.bound.left )
                this.bound.left = left;
            if( this.bound.right === false || right > this.bound.right )
                this.bound.right = right;
            if( this.bound.top === false || top < this.bound.top )
                this.bound.top = top;
            if( this.bound.bottom === false || bottom > this.bound.bottom )
                this.bound.bottom = bottom;
        },

        /**
         * Creates the canvas that sits on top of the game's canvas and holds game controls
         */
        createOverlayCanvas: function() {
            this.canvas = document.createElement( 'canvas' );

            // Scale to same size as original canvas
            this.resize( true );

            document.getElementsByTagName( 'body' )[0].appendChild( this.canvas );
            this.ctx = this.canvas.getContext( '2d' );

            var _this = this;
            window.addEventListener( 'resize', function() {
                // Wait for any other events to finish
                setTimeout( function() { GameController.resize.call( _this ); }, 1 );
            } );


            // Set the touch events for this new canvas
            this.setTouchEvents();

            // Load in the initial UI elements
            this.loadSide( 'left' );
            this.loadSide( 'right' );

            // Starts up the rendering / drawing
            this.render();

            if( ! this.touches || this.touches.length == 0 )
                this.paused = true; // pause until a touch event
        },

        pixelRatio: 1,
        resize: function( firstTime ) {
            // Scale to same size as original canvas
            this.canvas.width = this.options.canvas.width;
            this.canvas.height = this.options.canvas.height;

            this.offsetX = GameController.options.canvas.offsetLeft + document.body.scrollLeft;
            this.offsetY = GameController.options.canvas.offsetTop + document.body.scrollTop;

            // Get in on this retina action
            if( this.options.canvas.style.width && this.options.canvas.style.height && this.options.canvas.style.height.indexOf( 'px' ) !== -1 )
            {
                this.canvas.style.width = this.options.canvas.style.width;
                this.canvas.style.height = this.options.canvas.style.height;
                this.pixelRatio = this.canvas.width / parseInt( this.canvas.style.width );
            }

            this.canvas.style.position = 'absolute';
            this.canvas.style.zIndex = '5';
            this.canvas.style.left = this.options.canvas.offsetLeft + 'px';
            this.canvas.style.top = this.options.canvas.offsetTop + 'px';
            this.canvas.setAttribute( 'style', this.canvas.getAttribute( 'style' ) +' -ms-touch-action: none;' );

            if( !firstTime )
            {
                // Remove all current buttons
                this.touchableAreas = [];
                // Clear out the cached sprites
                this.cachedSprites = [];
                // Reload in the initial UI elements
                this.reloadSide( 'left' );
                this.reloadSide( 'right' );
            }
        },

        /**
         * Returns the scaled pixels. Given the value passed
         * @param {int/string} value - either an integer for # of pixels, or 'x%' for relative
         * @param {char} axis - x, y
         */
        getPixels: function( value, axis )
        {
            if( typeof value === 'undefined' )
                return 0
            else if( typeof value === 'number' )
                return value;
            else // a percentage
            {
                if( axis == 'x' )
                    return ( parseInt( value ) / 100 ) * this.canvas.width;
                else
                    return ( parseInt( value ) / 100 ) * this.canvas.height;
            }
        },

        /**
         * Simulates a key press
         * @param {string} eventName - 'down', 'up'
         * @param {char} character
         */
        simulateKeyEvent: function( eventName, keyCode ) {
            if( typeof window.onkeydown === 'undefined' ) // No keyboard, can't simulate...
                return false;

            /* If they have jQuery, use it because it works better for mobile safari */
            if( typeof jQuery !== 'undefined' )
            {
                var press = jQuery.Event( 'key' + eventName );
                press.ctrlKey = false;
                press.which = keyCode;
                press.keyCode = keyCode;
                // Keypress on just the canvas instead of the document
                $( this.options.canvas ).trigger( press );
                return;
            }

            var oEvent = document.createEvent( 'KeyboardEvent' );

            // Chromium Hack
            if( navigator.userAgent.toLowerCase().indexOf( 'chrome' ) !== -1 )
            {
                Object.defineProperty( oEvent, 'keyCode', {
                    get : function() {
                        return this.keyCodeVal;
                    }
                } );
                Object.defineProperty( oEvent, 'which', {
                    get : function() {
                        return this.keyCodeVal;
                    }
                } );
            }

            if( oEvent.initKeyboardEvent )
            {
                oEvent.initKeyboardEvent( 'key' + eventName, true, true, document.defaultView, false, false, false, false, keyCode, keyCode );
            }
            else
            {
                oEvent.initKeyEvent( 'key' + eventName, true, true, document.defaultView, false, false, false, false, keyCode, keyCode );
            }

            oEvent.keyCodeVal = keyCode;

        },

        setTouchEvents: function() {
            var _this = this;
            var touchStart = function( e ) {
                if( _this.paused )
                {
                    _this.paused = false;
                }

                e.preventDefault();

                // Microsoft always has to have their own stuff...
                if( window.navigator.msPointerEnabled && e.clientX && e.pointerType == e.MSPOINTER_TYPE_TOUCH )
                {
                    _this.touches[ e.pointerId ] = { clientX: e.clientX, clientY: e.clientY };
                }
                else
                {
                    _this.touches = e.touches || [];
                }
            };

            this.canvas.addEventListener( 'touchstart', touchStart, false );

            var touchEnd = function( e ) {
                e.preventDefault();

                if( window.navigator.msPointerEnabled && e.pointerType == e.MSPOINTER_TYPE_TOUCH )
                {
                    delete _this.touches[ e.pointerId ];
                }
                else
                {
                    _this.touches = e.touches || [];
                }

                if( !e.touches || e.touches.length == 0 )
                {
                    // Draw once more to remove the touch area
                    _this.render();
                    _this.paused = true;
                }
            };
            this.canvas.addEventListener( 'touchend', touchEnd );

            var touchMove = function( e ) {
                e.preventDefault();

                if( window.navigator.msPointerEnabled && e.clientX && e.pointerType == e.MSPOINTER_TYPE_TOUCH )
                {
                    _this.touches[ e.pointerId ] = { clientX: e.clientX, clientY: e.clientY };
                }
                else
                {
                    _this.touches = e.touches || [];
                }
            };
            this.canvas.addEventListener( 'touchmove', touchMove );

            if( window.navigator.msPointerEnabled )
            {
                this.canvas.addEventListener( 'MSPointerDown', touchStart );
                this.canvas.addEventListener( 'MSPointerUp', touchEnd );
                this.canvas.addEventListener( 'MSPointerMove', touchMove );
            }
        },

        /**
         * Adds the area to a list of touchable areas, draws
         * @param {object} options with properties: x, y, width, height, touchStart, touchEnd, touchMove
         */
        addTouchableDirection: function( options ) {

            var direction = new TouchableDirection( options );

            direction.id = this.touchableAreas.push( direction );
            this.touchableAreasCount++;

            this.boundingSet( options );
        },

        /**
         * Adds the circular area to a list of touchable areas, draws
         * @param {object} options with properties: x, y, width, height, touchStart, touchEnd, touchMove
         */
        addJoystick: function( options ) { //x, y, radius, backgroundColor, touchStart, touchEnd ) {

            var joystick = new TouchableJoystick( options );

            joystick.id = this.touchableAreas.push( joystick );
            this.touchableAreasCount++;

            this.boundingSet( options );
        },

        /**
         * Adds the circular area to a list of touchable areas, draws
         * @param {object} options with properties: x, y, width, height, touchStart, touchEnd, touchMove
         */
        addButton: function( options ) { //x, y, radius, backgroundColor, touchStart, touchEnd ) {

            var button = new TouchableButton( options );

            button.id = this.touchableAreas.push( button );
            this.touchableAreasCount++;

            this.boundingSet( options );
        },

        addTouchableArea: function( check, callback ) {
        },

        loadButtons: function( side ) {
            var buttons = this.options[ side ].buttons;
            var _this = this;
            for( var i = 0, j = buttons.length; i < j; i++ )
            {
                if( typeof buttons[i] === 'undefined' || typeof buttons[i].offset === 'undefined' )
                    continue;

                var posX = this.getPositionX( side );
                var posY = this.getPositionY( side );

                buttons[i].x = posX + this.getPixels( buttons[i].offset.x, 'y' );
                buttons[i].y = posY + this.getPixels( buttons[i].offset.y, 'y' );

                this.addButton( buttons[i] );
            }
        },

        loadDPad: function( side ) {
            var dpad = this.options[ side ].dpad || {};

            // Centered value is at this.options[ side ].position

            var _this = this;

            var posX = this.getPositionX( side );
            var posY = this.getPositionY( side );


            // If they have all 4 directions, add a circle to the center for looks
            if( dpad.up && dpad.left && dpad.down && dpad.right )
            {
                var options = {
                    x: posX,
                    y: posY,
                    radius: dpad.right.height
                }
                var center = new TouchableCircle( options );
                this.touchableAreas.push( center );
                this.touchableAreasCount++;
            }

            // Up arrow
            if( dpad.up !== false )
            {
                dpad.up.x = posX - this.getPixels( dpad.up.width, 'y' ) / 2;
                dpad.up.y = posY - ( this.getPixels( dpad.up.height, 'y' ) +  this.getPixels( dpad.left.height, 'y' ) / 2 );
                dpad.up.direction = 'up';
                this.addTouchableDirection( dpad.up );
            }

            // Left arrow
            if( dpad.left !== false )
            {
                dpad.left.x = posX - ( this.getPixels( dpad.left.width, 'y' ) + this.getPixels( dpad.up.width, 'y' ) / 2 );
                dpad.left.y = posY - ( this.getPixels( dpad.left.height, 'y' ) / 2 );
                dpad.left.direction = 'left';
                this.addTouchableDirection( dpad.left );
            }

            // Down arrow
            if( dpad.down !== false )
            {
                dpad.down.x = posX - this.getPixels( dpad.down.width, 'y' ) / 2;
                dpad.down.y = posY + ( this.getPixels( dpad.left.height, 'y' ) / 2 );
                dpad.down.direction = 'down';
                this.addTouchableDirection( dpad.down );
            }

            // Right arrow
            if( dpad.right !== false )
            {
                dpad.right.x = posX + ( this.getPixels( dpad.up.width, 'y' ) / 2 );
                dpad.right.y = posY - this.getPixels( dpad.right.height, 'y' ) / 2;
                dpad.right.direction = 'right';
                this.addTouchableDirection( dpad.right );
            }

        },

        loadJoystick: function( side ) {
            var joystick = this.options[ side ].joystick;
            joystick.x = this.getPositionX( side );
            joystick.y = this.getPositionY( side );

            this.addJoystick( joystick );
        },

        /**
         * Used for resizing. Currently is just an alias for loadSide
         */
        reloadSide: function( side ) {
            // Load in new ones
            this.loadSide( side );
        },

        loadSide: function( side ) {
            if( this.options[ side ].type === 'dpad' )
            {
                this.loadDPad( side );
            }
            else if( this.options[ side ].type === 'joystick' )
            {
                this.loadJoystick( side );
            }
            else if( this.options[ side ].type === 'buttons' )
            {
                this.loadButtons( side );
            }
        },

        /**
         * Normalize touch positions by the left and top offsets
         * @param {int} x
         */
        normalizeTouchPositionX: function( x )
        {
            return ( x - this.offsetX ) * ( this.pixelRatio );
        },

        /**
         * Normalize touch positions by the left and top offsets
         * @param {int} y
         */
        normalizeTouchPositionY: function( y )
        {
            return ( y - this.offsetY ) * ( this.pixelRatio );
        },

        /**
         * Returns the x position when given # of pixels from right (based on canvas size)
         * @param {int} right
         */
        getXFromRight: function( right ) {
            return this.canvas.width - right;
        },


        /**
         * Returns the y position when given # of pixels from bottom (based on canvas size)
         * @param {int} right
         */
        getYFromBottom: function( bottom ) {
            return this.canvas.height - bottom;
        },

        /**
         * Grabs the x position of either the left or right side/controls
         * @param {string} side - 'left', 'right'
         */
        getPositionX: function( side ) {
            if( typeof this.options[ side ].position.left !== 'undefined' )
                return this.getPixels( this.options[ side ].position.left, 'x' );
            else
                return this.getXFromRight( this.getPixels( this.options[ side ].position.right, 'x' ) );
        },

        /**
         * Grabs the y position of either the left or right side/controls
         * @param {string} side - 'left', 'right'
         */
        getPositionY: function( side ) {
            if( typeof this.options[ side ].position.top !== 'undefined' )
                return this.getPixels( this.options[ side ].position.top, 'y' );
            else
                return this.getYFromBottom( this.getPixels( this.options[ side ].position.bottom, 'y' ) );
        },

        /**
         * Processes the info for each touchableArea
         */
        renderAreas: function() {
            for( var i = 0, j = this.touchableAreasCount; i < j; i++ )
            {
                var area = this.touchableAreas[ i ];

                if( typeof area === 'undefined' )
                    continue;

                area.draw();

                // Go through all touches to see if any hit this area
                var touched = false;
                for( var k = 0, l = this.touches.length; k < l; k++ )
                {
                    var touch = this.touches[ k ];
                    if( typeof touch === 'undefined' )
                        continue;

                    var x = this.normalizeTouchPositionX( touch.clientX ), y = this.normalizeTouchPositionY( touch.clientY );

                    // Check that it's in the bounding box/circle
                    if( ( area.check( x, y ) ) !== false )
                    {
                        if( !touched )
                            touched = this.touches[ k ];
                    }
                }

                if( touched )
                {
                    if( !area.active )
                        area.touchStartWrapper( touched );
                    area.touchMoveWrapper( touched );
                }
                else if( area.active )
                {
                    area.touchEndWrapper( touched );
                }
            }
        },

        render: function() {
            if( ! this.paused || ! this.performanceFriendly )
                this.ctx.clearRect( this.bound.left, this.bound.top, this.bound.right - this.bound.left, this.bound.bottom - this.bound.top );

            // Draw feedback for when screen is being touched
            // When no touch events are happening, this enables 'paused' mode, which skips running this
            // This isn't run at all in performanceFriendly mode
            if( ! this.paused && ! this.performanceFriendly )
            {
                var cacheId = 'touch-circle';
                var cached = this.cachedSprites[ cacheId ];

                if( ! cached && this.options.touchRadius )
                {
                    var subCanvas = document.createElement( 'canvas' );
                    var ctx = subCanvas.getContext( '2d' );
                    subCanvas.width = 2 * this.options.touchRadius;
                    subCanvas.height = 2 * this.options.touchRadius;

                    var center = this.options.touchRadius;
                    var gradient = ctx.createRadialGradient( center, center, 1, center, center, this.options.touchRadius ); // 10 = end radius
                    gradient.addColorStop( 0, 'rgba( 200, 200, 200, 1 )' );
                    gradient.addColorStop( 1, 'rgba( 200, 200, 200, 0 )' );
                    ctx.beginPath();
                    ctx.fillStyle = gradient;
                    ctx.arc( center, center, this.options.touchRadius, 0 , 2 * Math.PI, false );
                    ctx.fill();

                    cached = GameController.cachedSprites[ cacheId ] = subCanvas;
                }
                // Draw the current touch positions if any
                for( var i = 0, j = this.touches.length; i < j; i++ )
                {
                    var touch = this.touches[ i ];
                    if( typeof touch === 'undefined' )
                        continue;
                    var x = this.normalizeTouchPositionX( touch.clientX ), y = this.normalizeTouchPositionY( touch.clientY );
                    if( x - this.options.touchRadius > this.bound.left && x + this.options.touchRadius < this.bound.right &&
                        y - this.options.touchRadius > this.bound.top && y + this.options.touchRadius < this.bound.bottom )
                        this.ctx.drawImage( cached, x - this.options.touchRadius, y - this.options.touchRadius );
                }
            }

            // Render if the game isn't paused, or we're not in performanceFriendly mode (running when not paused keeps the semi-transparent gradients looking better for some reason)
            if( ! this.paused || ! this.performanceFriendly )
            {
                // Process all the info for each touchable area
                this.renderAreas();
            }

            window.requestAnimationFrame( this.renderWrapper );
        },
        /**
         * So we can keep scope, and don't have to create a new obj every requestAnimationFrame (bad for garbage collection)
         */
        renderWrapper: function() {
            GameController.render();
        },
    }

    /**
     * Superclass for touchable stuff
     */
    var TouchableArea = ( function() {

        function TouchableArea()
        {
        }

        // Called when this direction is being touched
        TouchableArea.prototype.touchStart = null;

        // Called when this direction is being moved
        TouchableArea.prototype.touchMove = null;

        // Called when this direction is no longer being touched
        TouchableArea.prototype.touchEnd = null;

        TouchableArea.prototype.type = 'area';
        TouchableArea.prototype.id = false;
        TouchableArea.prototype.active = false;

        /**
         * Sets the user-specified callback for this direction being touched
         * @param {function} callback
         */
        TouchableArea.prototype.setTouchStart = function( callback ) {
            this.touchStart = callback;
        };

        /**
         * Called when this direction is no longer touched
         */
        TouchableArea.prototype.touchStartWrapper = function( e ) {
            // Fire the user specified callback
            if( this.touchStart )
                this.touchStart();

            // Mark this direction as active
            this.active = true;
        };

        /**
         * Sets the user-specified callback for this direction no longer being touched
         * @param {function} callback
         */
        TouchableArea.prototype.setTouchMove = function( callback ) {
            this.touchMove = callback;
        };

        /**
         * Called when this direction is moved. Make sure it's actually changed before passing to developer
         */
        TouchableArea.prototype.lastPosX = 0;
        TouchableArea.prototype.lastPosY = 0;
        TouchableArea.prototype.touchMoveWrapper = function( e ) {
            // Fire the user specified callback
            if( this.touchMove && ( e.clientX != TouchableArea.prototype.lastPosX || e.clientY != TouchableArea.prototype.lastPosY ) )
            {
                this.touchMove();
                this.lastPosX = e.clientX;
                this.lastPosY = e.clientY;
            }
            // Mark this direction as active
            this.active = true;
        };

        /**
         * Sets the user-specified callback for this direction no longer being touched
         * @param {function} callback
         */
        TouchableArea.prototype.setTouchEnd = function( callback ) {
            this.touchEnd = callback;
        };

        /**
         * Called when this direction is first touched
         */
        TouchableArea.prototype.touchEndWrapper = function( e ) {
            // Fire the user specified callback
            if( this.touchEnd )
                this.touchEnd();

            // Mark this direction as inactive
            this.active = false;

            GameController.render();
        };

        return TouchableArea;

    } )();

    var TouchableDirection = ( function( __super ) {
        __extends( TouchableDirection, __super );

        function TouchableDirection( options )
        {
            for( var i in options )
            {
                if( i == 'x' )
                    this[i] = GameController.getPixels( options[i], 'x' );
                else if( i == 'y' || i == 'height' || i == 'width' )
                    this[i] = GameController.getPixels( options[i], 'y' );
                else
                    this[i] = options[i];
            }

            this.draw();
        }

        TouchableDirection.prototype.type = 'direction';

        /**
         * Checks if the touch is within the bounds of this direction
         */
        TouchableDirection.prototype.check = function( touchX, touchY ) {
            var distanceX, distanceY;
            if( ( Math.abs( touchX - this.x ) < ( GameController.options.touchRadius / 2 ) || ( touchX > this.x ) ) && // left
                ( Math.abs( touchX - ( this.x + this.width ) ) < ( GameController.options.touchRadius / 2 ) || ( touchX < this.x + this.width ) ) && // right
                ( Math.abs( touchY - this.y ) < ( GameController.options.touchRadius / 2 ) || ( touchY > this.y ) ) && // top
                ( Math.abs( touchY - ( this.y + this.height ) ) < ( GameController.options.touchRadius / 2 ) || ( touchY < this.y + this.height ) ) // bottom
            )
                return true;

            return false;
        };

        TouchableDirection.prototype.draw = function() {
            var cacheId = this.type + '' + this.id + '' + this.active;
            var cached = GameController.cachedSprites[ cacheId ];
            if( ! cached )
            {
                var subCanvas = document.createElement( 'canvas' );
                var ctx = subCanvas.getContext( '2d' );
                subCanvas.width = this.width + 2 * this.stroke;
                subCanvas.height = this.height + 2 * this.stroke;

                var opacity = this.opacity || 0.9;

                if( ! this.active ) // Direction currently being touched
                    opacity *= 0.5;

                switch( this.direction )
                {
                    case 'up':
                        var gradient = ctx.createLinearGradient( 0, 0, 0, this.height );
                        gradient.addColorStop( 0, 'rgba( 0, 0, 0, ' + ( opacity * 0.5 ) + ' )' );
                        gradient.addColorStop( 1, 'rgba( 0, 0, 0, ' + opacity + ' )' );
                        break;
                    case 'left':
                        var gradient = ctx.createLinearGradient( 0, 0, this.width, 0 );
                        gradient.addColorStop( 0, 'rgba( 0, 0, 0, ' + ( opacity * 0.5 ) + ' )' );
                        gradient.addColorStop( 1, 'rgba( 0, 0, 0, ' + opacity + ' )' );
                        break;
                    case 'right':
                        var gradient = ctx.createLinearGradient( 0, 0, this.width, 0 );
                        gradient.addColorStop( 0, 'rgba( 0, 0, 0, ' + opacity + ' )' );
                        gradient.addColorStop( 1, 'rgba( 0, 0, 0, ' + ( opacity * 0.5 ) + ' )' );
                        break;
                    case 'down':
                    default:
                        var gradient = ctx.createLinearGradient( 0, 0, 0, this.height );
                        gradient.addColorStop( 0, 'rgba( 0, 0, 0, ' + opacity + ' )' );
                        gradient.addColorStop( 1, 'rgba( 0, 0, 0, ' + ( opacity * 0.5 ) + ' )' );
                }
                ctx.fillStyle = gradient;

                ctx.fillRect( 0, 0, this.width, this.height );
                ctx.lineWidth = this.stroke;
                ctx.strokeStyle = 'rgba( 255, 255, 255, 0.1 )';
                ctx.strokeRect( 0, 0, this.width, this.height );

                cached = GameController.cachedSprites[ cacheId ] = subCanvas;
            }

            GameController.ctx.drawImage( cached, this.x, this.y );


        };

        return TouchableDirection;
    } )( TouchableArea );

    var TouchableButton = ( function( __super ) {
        __extends( TouchableButton, __super );

        function TouchableButton( options ) //x, y, radius, backgroundColor )
        {
            for( var i in options )
            {
                if( i == 'x' )
                    this[i] = GameController.getPixels( options[i], 'x' );
                else if( i == 'x' || i == 'radius' )
                    this[i] = GameController.getPixels( options[i], 'y' );
                else
                    this[i] = options[i];
            }

            this.draw();
        }

        TouchableButton.prototype.type = 'button';

        /**
         * Checks if the touch is within the bounds of this direction
         */
        TouchableButton.prototype.check = function( touchX, touchY ) {
            if(
                ( Math.abs( touchX - this.x ) < this.radius + ( GameController.options.touchRadius / 2 ) ) &&
                ( Math.abs( touchY - this.y ) < this.radius + ( GameController.options.touchRadius / 2 ) )
            )
                return true;

            return false;
        };

        TouchableButton.prototype.draw = function() {
            var cacheId = this.type + '' + this.id + '' + this.active;
            var cached = GameController.cachedSprites[ cacheId ];
            if( ! cached )
            {
                var subCanvas = document.createElement( 'canvas' );
                var ctx = subCanvas.getContext( '2d' );
                ctx.lineWidth = this.stroke;
                subCanvas.width = subCanvas.height = 2 * ( this.radius + ctx.lineWidth );


                var gradient = ctx.createRadialGradient( this.radius, this.radius, 1, this.radius, this.radius, this.radius );
                var textShadowColor;
                switch( this.backgroundColor )
                {
                    case 'blue':
                        gradient.addColorStop( 0, 'rgba(123, 181, 197, 0.6)' );
                        gradient.addColorStop( 1, '#105a78' );
                        textShadowColor = '#0A4861';
                        break;
                    case 'green':
                        gradient.addColorStop( 0, 'rgba(29, 201, 36, 0.6)' );
                        gradient.addColorStop( 1, '#107814' );
                        textShadowColor = '#085C0B';
                        break;
                    case 'red':
                        gradient.addColorStop( 0, 'rgba(165, 34, 34, 0.6)' );
                        gradient.addColorStop( 1, '#520101' );
                        textShadowColor = '#330000';
                        break;
                    case 'yellow':
                        gradient.addColorStop( 0, 'rgba(219, 217, 59, 0.6)' );
                        gradient.addColorStop( 1, '#E8E10E' );
                        textShadowColor = '#BDB600';
                        break;
                    case 'white':
                    default:
                        gradient.addColorStop( 0, 'rgba( 255,255,255,.3 )' );
                        gradient.addColorStop( 1, '#eee' );
                        break;
                }

                if( this.active )
                    ctx.fillStyle = textShadowColor;
                else
                    ctx.fillStyle = gradient;

                ctx.strokeStyle = textShadowColor;

                ctx.beginPath();
                //ctx.arc( this.x, this.y, this.radius, 0 , 2 * Math.PI, false );
                ctx.arc( subCanvas.width / 2, subCanvas.width / 2, this.radius, 0 , 2 * Math.PI, false );
                ctx.fill();
                ctx.stroke();

                if( this.label )
                {
                    // Text Shadow
                    ctx.fillStyle = textShadowColor;
                    ctx.font = 'bold ' + ( this.fontSize || subCanvas.height * 0.35 ) + 'px Verdana';
                    ctx.textAlign = 'center';
                    ctx.textBaseline = 'middle';
                    ctx.fillText( this.label, subCanvas.height / 2 + 2, subCanvas.height / 2 + 2 );


                    ctx.fillStyle = this.fontColor;
                    ctx.font = 'bold ' + ( this.fontSize || subCanvas.height * 0.35 ) + 'px Verdana';
                    ctx.textAlign = 'center';
                    ctx.textBaseline = 'middle';
                    ctx.fillText( this.label, subCanvas.height / 2, subCanvas.height / 2 );
                }

                cached = GameController.cachedSprites[ cacheId ] = subCanvas;
            }

            GameController.ctx.drawImage( cached, this.x, this.y );


        };

        return TouchableButton;
    } )( TouchableArea );

    var TouchableJoystick = ( function( __super ) {
        __extends( TouchableJoystick, __super );

        function TouchableJoystick( options ) //x, y, radius, backgroundColor )
        {
            for( var i in options )
                this[i] = options[i];

            this.currentX = this.currentX || this.x;
            this.currentY = this.currentY || this.y;
        }

        TouchableJoystick.prototype.type = 'joystick';

        /**
         * Checks if the touch is within the bounds of this direction
         */
        TouchableJoystick.prototype.check = function( touchX, touchY ) {
            if(
                ( Math.abs( touchX - this.x ) < this.radius + ( GameController.getPixels( GameController.options.touchRadius ) / 2 ) ) &&
                ( Math.abs( touchY - this.y ) < this.radius + ( GameController.getPixels( GameController.options.touchRadius ) / 2 ) )
            )
                return true;

            return false;
        };

        /**
         * details for the joystick move event, stored here so we're not constantly creating new objs for garbage. The object has params:
         * dx - the number of pixels the current joystick center is from the base center in x direction
         * dy - the number of pixels the current joystick center is from the base center in y direction
         * max - the maximum number of pixels dx or dy can be
         * normalizedX - a number between -1 and 1 relating to how far left or right the joystick is
         * normalizedY - a number between -1 and 1 relating to how far up or down the joystick is
         */
        TouchableJoystick.prototype.moveDetails = {};

        /**
         * Called when this joystick is moved
         */
        TouchableJoystick.prototype.touchMoveWrapper = function( e ) {
            this.currentX = GameController.normalizeTouchPositionX( e.clientX );
            this.currentY = GameController.normalizeTouchPositionY( e.clientY );

            // Fire the user specified callback
            if( this.touchMove )
            {
                if( this.moveDetails.dx != this.currentX - this.x && this.moveDetails.dy != this.y - this.currentY )
                {
                    this.moveDetails.dx = this.currentX - this.x; // reverse so right is positive
                    this.moveDetails.dy = this.y - this.currentY;
                    this.moveDetails.max = this.radius + ( GameController.options.touchRadius / 2 );
                    this.moveDetails.normalizedX = this.moveDetails.dx / this.moveDetails.max;
                    this.moveDetails.normalizedY = this.moveDetails.dy / this.moveDetails.max;

                    this.touchMove( this.moveDetails );
                }
            }


            // Mark this direction as inactive
            this.active = true;
        };

        TouchableJoystick.prototype.draw = function() {
            if( ! this.id ) // wait until id is set
                return false;

            var cacheId = this.type + '' + this.id + '' + this.active;
            var cached = GameController.cachedSprites[ cacheId ];
            if( ! cached )
            {
                var subCanvas = document.createElement( 'canvas' );
                this.stroke = this.stroke || 2;
                subCanvas.width = subCanvas.height = 2 * ( this.radius + ( GameController.options.touchRadius ) + this.stroke );

                var ctx = subCanvas.getContext( '2d' );
                ctx.lineWidth = this.stroke;
                if( this.active ) // Direction currently being touched
                {
                    var gradient = ctx.createRadialGradient( 0, 0, 1, 0, 0, this.radius );
                    gradient.addColorStop( 0, 'rgba( 200,200,200,.5 )' );
                    gradient.addColorStop( 1, 'rgba( 200,200,200,.9 )' );
                    ctx.strokeStyle = '#000';
                }
                else
                {
                    // STYLING FOR BUTTONS
                    var gradient = ctx.createRadialGradient( 0, 0, 1, 0, 0, this.radius );
                    gradient.addColorStop( 0, 'rgba( 200,200,200,.2 )' );
                    gradient.addColorStop( 1, 'rgba( 200,200,200,.4 )' );
                    ctx.strokeStyle = 'rgba( 0,0,0,.4 )';
                }
                ctx.fillStyle = gradient;
                // Actual joystick part that is being moved
                ctx.beginPath();
                ctx.arc( this.radius, this.radius, this.radius, 0 , 2 * Math.PI, false );
                ctx.fill();
                ctx.stroke();

                cached = GameController.cachedSprites[ cacheId ] = subCanvas;
            }

            // Draw the base that stays static
            GameController.ctx.fillStyle = '#444';
            GameController.ctx.beginPath();
            GameController.ctx.arc( this.x, this.y, this.radius * 0.7, 0 , 2 * Math.PI, false );
            GameController.ctx.fill();
            GameController.ctx.stroke();

            GameController.ctx.drawImage( cached, this.currentX - this.radius, this.currentY - this.radius );


        };

        return TouchableJoystick;
    } )( TouchableArea );


    var TouchableCircle = ( function( __super ) {
        __extends( TouchableCircle, __super );

        function TouchableCircle( options )
        {
            for( var i in options )
            {
                if( i == 'x' )
                    this[i] = GameController.getPixels( options[i], 'x' );
                else if( i == 'x' || i == 'radius' )
                    this[i] = GameController.getPixels( options[i], 'y' );
                else
                    this[i] = options[i];
            }

            this.draw();
        }

        /**
         * No touch for this fella
         */
        TouchableCircle.prototype.check = function( touchX, touchY ) {
            return false;
        };

        TouchableCircle.prototype.draw = function() {

            // STYLING FOR BUTTONS
            GameController.ctx.fillStyle = 'rgba( 0, 0, 0, 0.5 )';

            // Actual joystick part that is being moved
            GameController.ctx.beginPath();
            GameController.ctx.arc( this.x, this.y, this.radius, 0 , 2 * Math.PI, false );
            GameController.ctx.fill();

        };

        return TouchableCircle;
    } )( TouchableArea );

    /**
     * Shim for requestAnimationFrame
     */
    ( function() {
        if (typeof module !== "undefined") return
        var lastTime = 0;
        var vendors = ['ms', 'moz', 'webkit', 'o'];
        for( var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x )
        {
            window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
            window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']
                || window[vendors[x]+'CancelRequestAnimationFrame'];
        }

        if ( !window.requestAnimationFrame )
            window.requestAnimationFrame = function( callback, element ) {
                var currTime = new Date().getTime();
                var timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) );
                var id = window.setTimeout( function() { callback(currTime + timeToCall); },
                    timeToCall );
                lastTime = currTime + timeToCall;
                return id;
            };

        if ( !window.cancelAnimationFrame )
            window.cancelAnimationFrame = function( id ) {
                clearTimeout( id );
            };
    }() );
} )(typeof module !== "undefined" ? module.exports : window)